﻿// <copyright file="GdalProvider.cs" company="Signal Processing Lab, Wuhan University">
// Copyright (c) 2009-2010 All Right Reserved
// </copyright>
// <author>Xinping Deng</author>
// <email>dentoby@gmail.com</email>
// <date>2010-3</date>
// <summary>This is part of Image.NET project.</summary>

namespace SPLWHU.Core.Raster
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Runtime.InteropServices;
    using OSGeo.GDAL;
    using SPLWHU.Core.Exceptions;

    /// <summary>
    /// The gdal file reader and writer.
    /// </summary>
    /// <typeparam name="T">The data type.</typeparam>
    public class GdalProvider<T> : IDataProvider<T>
    {
        /// <summary>
        /// The prefix of file name.
        /// </summary>
        private static readonly string FilenamePrefix = "GDAL_CACHE_";

        /// <summary>
        /// The gdal band data.
        /// </summary>
        private Band bandData;

        /// <summary>
        /// The widht and height of the image.
        /// </summary>
        private int width, height;

        /// <summary>
        /// Readonly or not.
        /// </summary>
        private bool readOnly = true;

        /// <summary>
        /// The data type.
        /// </summary>
        private int dataType;

        /// <summary>
        /// The filename of the created file.
        /// </summary>
        /// <remarks>
        /// The file are deleted when dispose this instance.
        /// </remarks>
        private string fileName;

        /// <summary>
        /// Initializes a new instance of the <see cref="GdalProvider&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="dataType">Type of the data.</param>
        public GdalProvider(int width, int height, int dataType)
        {
            // create the gdal data set.
            this.fileName = this.GenerateFileName();
            Dataset ds = this.CreateDataSet(this.fileName, width, height, 1, dataType);
           
            // the image information.
            this.readOnly = false;
            this.dataType = dataType;
            this.width = width;
            this.height = height;
            this.bandData = ds.GetRasterBand(1);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="GdalProvider&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="bandData">The band data.</param>
        public GdalProvider(int width, int height, Band bandData)
        {
            // the image information.
            this.readOnly = true;
            this.dataType = (int)bandData.DataType;
            this.width = width;
            this.height = height;
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="GdalProvider&lt;T&gt;"/> class.
        /// </summary>
        ~GdalProvider()
        {
            if (File.Exists(this.fileName))
            {
                File.Delete(this.fileName);
            }

            if (bandData != null)
            {
                bandData.Dispose();
            }
        }

        /// <summary>
        /// Gets the width of the image.
        /// </summary>
        /// <value>The width of the image.</value>
        public int ImageWidth
        {
            get { return this.width; }
        }

        /// <summary>
        /// Gets the height of the image.
        /// </summary>
        /// <value>The height of the image.</value>
        public int ImageHeight
        {
            get { return this.height; }
        }

        /// <summary>
        /// Gets or sets the row data T[] at the specified y.
        /// </summary>
        /// <param name="y">The row index</param>
        /// <returns>The row data.</returns>
        public T[] this[int y]
        {
            get
            {
                return this.ReadRowData(y);
            }

            set
            {
                this.WriteRowData(y, value);
            }
        }

        /// <summary>
        /// Loads the data set from file.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <param name="dataType">Type of the data.</param>
        /// <returns>The data set.</returns>
        public static Dataset ReadDataSet(string filename, ref int dataType)
        {
            try
            {
                /* -------------------------------------------------------------------- */
                /*      Register driver(s).                                             */
                /* -------------------------------------------------------------------- */
                Gdal.AllRegister();

                /* -------------------------------------------------------------------- */
                /*      Open dataset.                                                   */
                /* -------------------------------------------------------------------- */
                Dataset ds = Gdal.Open(filename, 0);

                if (ds == null)
                {
                    throw new Exception("Can't open " + filename);
                }

                /* -------------------------------------------------------------------- */
                /*      Get raster band                                                 */
                /* -------------------------------------------------------------------- */
                Band band = ds.GetRasterBand(1);
                dataType = (int)band.DataType;

                return ds;
            }
            catch (Exception e)
            {
                throw new Exception("Application error: " + e.Message);
            }
        }

        /// <summary>
        /// Gets the specified column data.
        /// </summary>
        /// <param name="x">The column index.</param>
        /// <returns>The column data.</returns>
        public T[] GetColumn(int x)
        {
            return this.ReadColumnData(x);
        }

        /// <summary>
        /// Sets the specifed column data.
        /// </summary>
        /// <param name="x">The column index.</param>
        /// <param name="data">The column data.</param>
        public void SetColumn(int x, T[] data)
        {
            if (this.readOnly)
            {
                throw new MatrixException("The file is read only.");
            }

            this.WriteColumnData(x, data);
        }

        /// <summary>
        /// Gets the data in the specified region.
        /// </summary>
        /// <param name="x">The starting column index.</param>
        /// <param name="y">The starting row index.</param>
        /// <param name="height">The height of the region.</param>
        /// <param name="width">The width of the region.</param>
        /// <returns>The data in the region.</returns>
        public Matrix<T> GetData(int x, int y, int height, int width)
        {
            T[][] data = this.ReadData(x, y, height, width);
            return Matrix<T>.CreateMatrix(data);
        }

        /// <summary>
        /// Sets the data in the specified region.
        /// </summary>
        /// <param name="x">The starting column index.</param>
        /// <param name="y">The starting row index.</param>
        /// <param name="data">The data to be filled.</param>
        public void SetData(int x, int y, Matrix<T> data)
        {
            if (this.readOnly)
            {
                throw new MatrixException("The file is read only.");
            }

            this.WriteData(x, y, (T[][])data);
        }

        /// <summary>
        /// Gets the data in the specifed region (interpolating).
        /// </summary>
        /// <param name="x">The starting column index.</param>
        /// <param name="y">The starting row index.</param>
        /// <param name="height">The height of the original region.</param>
        /// <param name="width">The width of the original region.</param>
        /// <param name="dstHeight">Height of the destination region.</param>
        /// <param name="dstWidth">Width of the destination region.</param>
        /// <returns>The interpolating data.</returns>
        public Matrix<T> GetData(int x, int y, int height, int width, int dstHeight, int dstWidth)
        {
            T[][] data = this.ReadData(x, y, height, width, dstHeight, dstWidth);
            return Matrix<T>.CreateMatrix(data);
        }

        /// <summary>
        /// Sets the data to the specifed region (interpolating).
        /// </summary>
        /// <param name="x">The starting column index.</param>
        /// <param name="y">The starting row index.</param>
        /// <param name="dstHeight">Height of the destination region.</param>
        /// <param name="dstWidth">Width of the destination region.</param>
        /// <param name="data">The data to be filled.</param>
        public void SetData(int x, int y, int dstHeight, int dstWidth, Matrix<T> data)
        {
            if (this.readOnly)
            {
                throw new MatrixException("The file is read only.");
            }

            this.WriteData(x, y, dstHeight, dstWidth, (T[][])data);
        }

        /// <summary>
        /// Creates the data set.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <param name="width">The width of the image.</param>
        /// <param name="height">The height of the image.</param>
        /// <param name="bandCount">The band count.</param>
        /// <param name="dataType">Type of the data.</param>
        /// <returns>The created dataset.</returns>
        private Dataset CreateDataSet(string fileName, int width, int height, int bandCount, int dataType)
        {
            try
            {
                /* -------------------------------------------------------------------- */
                /*      Register driver(s).                                             */
                /* -------------------------------------------------------------------- */
                Gdal.AllRegister();

                /* -------------------------------------------------------------------- */
                /*      Get driver                                                      */
                /* -------------------------------------------------------------------- */
                Driver drv = Gdal.GetDriverByName("GTiff");

                if (drv == null)
                {
                    throw new Exception("Can't get driver.");
                }

                /* -------------------------------------------------------------------- */
                /*      Open dataset.                                                   */
                /* -------------------------------------------------------------------- */
                string[] options = new string[] { "BLOCKXSIZE=" + width, "BLOCKYSIZE=" + 1 };
                Dataset ds = drv.Create(fileName, width, height, bandCount, (DataType)dataType, options);

                if (ds == null)
                {
                    throw new Exception("Can't open " + fileName);
                }

                return ds;
            }
            catch (Exception e)
            {
                throw new Exception("Application ERROR: " + e.Message);
            }
        }

        /// <summary>
        /// Reads the row data.
        /// </summary>
        /// <param name="y">The row index.</param>
        /// <returns>The row data.</returns>
        private T[] ReadRowData(int y)
        {
            // creates the buffer.
            T[] ret = new T[this.width];

            // locks the buffer.
            GCHandle handle = GCHandle.Alloc(ret, GCHandleType.Pinned);

            // reads data.
            IntPtr ptr = handle.AddrOfPinnedObject();
            this.bandData.ReadRaster(0, y, this.width, 1, ptr, this.width, 1, (DataType)this.dataType, 0, 0);

            // unlocks the buffer.
            if (handle.IsAllocated)
            {
                handle.Free();
            }

            // returns
            return ret;
        }

        /// <summary>
        /// Reads the column data.
        /// </summary>
        /// <param name="x">The column index.</param>
        /// <returns>The column data.</returns>
        private T[] ReadColumnData(int x)
        {
            // creates the buffer.
            T[] ret = new T[this.height];

            // locks the buffer.
            GCHandle handle = GCHandle.Alloc(ret, GCHandleType.Pinned);

            // reads data.
            IntPtr ptr = handle.AddrOfPinnedObject();
            this.bandData.ReadRaster(x, 0, 1, this.height, ptr, 1, this.height, (DataType)this.dataType, 0, 0);

            // unlocks the buffer.
            if (handle.IsAllocated)
            {
                handle.Free();
            }

            // returns
            return ret;
        }

        /// <summary>
        /// Reads the data in the specified region.
        /// </summary>
        /// <param name="x">The starting column index.</param>
        /// <param name="y">The staring row index.</param>
        /// <param name="height">The height of the region.</param>
        /// <param name="width">The width of the region.</param>
        /// <returns>The data in the region.</returns>
        private T[][] ReadData(int x, int y, int height, int width)
        {
            // creates the buffer.
            T[][] ret = new T[height][];

            for (int j = 0; j < height; j++)
            {
                ret[j] = new T[width];

                // locks the buffer.
                GCHandle handle = GCHandle.Alloc(ret[j], GCHandleType.Pinned);

                // reads data.
                IntPtr ptr = handle.AddrOfPinnedObject();
                this.bandData.ReadRaster(x, y + j, width, 1, ptr, width, 1, (DataType)this.dataType, 0, 0);

                // unlocks the buffer.
                if (handle.IsAllocated)
                {
                    handle.Free();
                }
            }

            // returns
            return ret;
        }

        /// <summary>
        /// Reads the data in the specified region (interpolating).
        /// </summary>
        /// <param name="x">The starting column index.</param>
        /// <param name="y">The staring row index.</param>
        /// <param name="height">The height of original region.</param>
        /// <param name="width">The width of original region.</param>
        /// <param name="dstHeight">Height of the destination region.</param>
        /// <param name="dstWidth">Width of the destination region.</param>
        /// <returns>The interpolating data.</returns>
        private T[][] ReadData(int x, int y, int height, int width, int dstHeight, int dstWidth)
        {
            // creates the buffer.
            T[][] ret = new T[dstHeight][];
            double rowStep = (double)height / dstHeight;
            double rowOff = y;

            for (int j = 0; j < dstHeight; j++)
            {
                ret[j] = new T[dstWidth];

                rowOff += rowStep * j;

                // locks the buffer.
                GCHandle handle = GCHandle.Alloc(ret[j], GCHandleType.Pinned);

                // reads data.
                IntPtr ptr = handle.AddrOfPinnedObject();
                this.bandData.ReadRaster(x, (int)rowOff, width, 1, ptr, dstWidth, 1, (DataType)this.dataType, 0, 0);

                // unlocks the buffer.
                if (handle.IsAllocated)
                {
                    handle.Free();
                }
            }

            // returns
            return ret;
        }

        /// <summary>
        /// Writes the row data.
        /// </summary>
        /// <param name="y">The row index.</param>
        /// <param name="data">The row data.</param>
        private void WriteRowData(int y, T[] data)
        {
            // locks the buffer.
            GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);

            // reads data.
            IntPtr ptr = handle.AddrOfPinnedObject();
            this.bandData.WriteRaster(0, y, this.width, 1, ptr, this.width, 1, (DataType)this.dataType, 0, 0);

            // unlocks the buffer.
            if (handle.IsAllocated)
            {
                handle.Free();
            }
        }

        /// <summary>
        /// Writes the column data.
        /// </summary>
        /// <param name="x">The column index.</param>
        /// <param name="data">The column data.</param>
        private void WriteColumnData(int x, T[] data)
        {
            // locks the buffer.
            GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);

            // reads data.
            IntPtr ptr = handle.AddrOfPinnedObject();
            this.bandData.WriteRaster(x, 0, 1, this.height, ptr, 1, this.height, (DataType)this.dataType, 0, 0);

            // unlocks the buffer.
            if (handle.IsAllocated)
            {
                handle.Free();
            }
        }

        /// <summary>
        /// Writes the data to the specified region.
        /// </summary>
        /// <param name="x">The starting column index.</param>
        /// <param name="y">The starting row index.</param>
        /// <param name="data">The data to be written.</param>
        private void WriteData(int x, int y, T[][] data)
        {
            int height = data.Length, width = data[0].Length;
            for (int j = 0; j < height; j++)
            {
                T[] row = data[j];

                // locks the buffer.
                GCHandle handle = GCHandle.Alloc(row, GCHandleType.Pinned);

                // reads data.
                IntPtr ptr = handle.AddrOfPinnedObject();
                this.bandData.WriteRaster(x, y + j, width, 1, ptr, width, 1, (DataType)this.dataType, 0, 0);

                // unlocks the buffer.
                if (handle.IsAllocated)
                {
                    handle.Free();
                }
            }
        }

        /// <summary>
        /// Writes the data to a region (interpolating).
        /// </summary>
        /// <param name="x">The starting column index.</param>
        /// <param name="y">The starting row index.</param>
        /// <param name="dstHeight">Height of the destination region.</param>
        /// <param name="dstWidth">Width of the destination region.</param>
        /// <param name="data">The data to be written.</param>
        private void WriteData(int x, int y, int dstHeight, int dstWidth, T[][] data)
        {
            int height = data.Length, width = data[0].Length;

            double rowStep = (double)height / dstHeight;
            double rowOff = 0;

            for (int j = 0; j < dstHeight; j++)
            {
                rowOff += rowStep * j;

                // locks the buffer.
                GCHandle handle = GCHandle.Alloc(data[(int)rowOff], GCHandleType.Pinned);

                // reads data.
                IntPtr ptr = handle.AddrOfPinnedObject();
                this.bandData.WriteRaster(x, y + j, dstWidth, 1, ptr, width, 1, (DataType)this.dataType, 0, 0);

                // unlocks the buffer.
                if (handle.IsAllocated)
                {
                    handle.Free();
                }
            }
        }

        /// <summary>
        /// Generates the name of the file.
        /// </summary>
        /// <returns>The file name.</returns>
        private string GenerateFileName()
        {
            if (!Directory.Exists("Cache"))
            {
                Directory.CreateDirectory("Cache");
                return GdalProvider<T>.FilenamePrefix + "0";
            }

            int index = 0;
            while (true)
            {
                string name = "Cache\\" + GdalProvider<T>.FilenamePrefix + index.ToString();
                if (!File.Exists(name))
                {
                    return name;
                }
                else
                {
                    index++;
                }
            }
        }
    }
}
